/**
 * Class: BeadGlide
 *
 * Purpose: 
 *
 * Created: Jan 28, 2010 @ 10:05:30 PM
 * @author Ken Scott
 */
package org.openfantasia.musica;

import net.beadsproject.beads.core.AudioContext;
import net.beadsproject.beads.ugens.*;
import org.openfantasia.musica.events.Event;
import org.openfantasia.musica.events.EventList;
import org.openfantasia.util.math.PMath;
import org.openfantasia.util.time.PrecisionClock;
import org.openfantasia.util.time.PrecisionClockListener;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Musica {

	// ******************************************************************************************************************
	// CONSTANTS

	public static final int octaveMin = 0;

	public static final int octaveMax = 10;

	public static final int octaveCount = octaveMax - octaveMin + 1;

	public static final int tonesPerOctave = 12;

	public static final double baseFrequency = 27.5f;

	public static final String[][] NOTE_NAMES_12TET = {
			{"A"},
			{"Ab", "B#", "As", "Bf", "Ad"},
			{"B", "C#"},
			{"C", "Bb", "Bd"},
			{"Cb", "D#", "Cs", "Df", "Cd"},
			{"D"},
			{"Db", "E#", "Ds", "Ef", "Dd"},
			{"E", "F#"},
			{"F", "Eb", "Ed"},
			{"Fb", "G#", "Fs", "Gf", "Fd"},
			{"G"},
			{"Gb", "A#", "Gs", "Af", "Gd"},
	};

	public static final String MAJOR = "Major";

	public static final String MINOR = "Minor";

	public static final String NATURAL_MINOR = "Natural Minor";

	public static final String HARMONIC_MINOR = "Harmonic Minor";

	public static final String MELODIC_MINOR = "Melodic Minor";

	public static final String IONIAN = "Ionian";

	public static final String DORIAN = "Dorian";

	public static final String PHRYGIAN = "Phrygian";

	public static final String LYDIAN = "Lydian";

	public static final String MIXOLYDIAN = "Mixolydian";

	public static final String AEOLIAN = "Aeolian";

	public static final String LOCRIAN = "Locrian";

	public static final String[] SCALE_TYPE_NAMES = {
			MAJOR,
			MINOR,
			NATURAL_MINOR,
			HARMONIC_MINOR,
			MELODIC_MINOR,
			IONIAN,
			DORIAN,
			PHRYGIAN,
			LYDIAN,
			MIXOLYDIAN,
			AEOLIAN,
			LOCRIAN,
	};

	public static final int[] MAJOR_SCALE_PATTERN = {2, 2, 1, 2, 2, 2, 1};

	public static final int[] NATURAL_MINOR_SCALE_PATTERN = {2, 1, 2, 2, 1, 2, 2};

	public static final int[] HARMONIC_MINOR_SCALE_PATTERN = {2, 1, 2, 2, 1, 3, 1};

	public static final int[] MELODIC_MINOR_SCALE_PATTERN = {2, 1, 2, 2, 2, 2, 1};

	public static final int[] IONIAN_SCALE_PATTERN = {2, 2, 1, 2, 2, 2, 1};

	public static final int[] DORIAN_SCALE_PATTERN = {2, 1, 2, 2, 2, 1, 2};

	public static final int[] PHRYGIAN_SCALE_PATTERN = {1, 2, 2, 2, 1, 2, 2};

	public static final int[] LYDIAN_SCALE_PATTERN = {2, 2, 2, 1, 2, 2, 1};

	public static final int[] MIXOLYDIAN_SCALE_PATTERN = {2, 2, 1, 2, 2, 1, 2};

	public static final int[] AEOLIAN_SCALE_PATTERN = {2, 1, 2, 2, 1, 2, 2};

	public static final int[] LOCRIAN_SCALE_PATTERN = {1, 2, 2, 1, 2, 2, 2};

	public static final Map<String, int[]> scalePatternMap = new HashMap<String, int[]>();


	static {
		scalePatternMap.put(MAJOR, MAJOR_SCALE_PATTERN);
		scalePatternMap.put(NATURAL_MINOR, NATURAL_MINOR_SCALE_PATTERN);
		scalePatternMap.put(MINOR, NATURAL_MINOR_SCALE_PATTERN);
		scalePatternMap.put(HARMONIC_MINOR, HARMONIC_MINOR_SCALE_PATTERN);
		scalePatternMap.put(MELODIC_MINOR, MELODIC_MINOR_SCALE_PATTERN);
		scalePatternMap.put(IONIAN, IONIAN_SCALE_PATTERN);
		scalePatternMap.put(DORIAN, DORIAN_SCALE_PATTERN);
		scalePatternMap.put(PHRYGIAN, PHRYGIAN_SCALE_PATTERN);
		scalePatternMap.put(LYDIAN, LYDIAN_SCALE_PATTERN);
		scalePatternMap.put(MIXOLYDIAN, MIXOLYDIAN_SCALE_PATTERN);
		scalePatternMap.put(AEOLIAN, AEOLIAN_SCALE_PATTERN);
		scalePatternMap.put(LOCRIAN, LOCRIAN_SCALE_PATTERN);

	}


	public static final String[] INTERVAL_NAMES = {
			"tone",
			"minor second",
			"major second",
			"minor third",
			"major third",
			"fourth",
			"tritone",
			"fifth",
			"minor sixth",
			"major sixth",
			"minor seventh",
			"major seventh",
			"octave",
			"minor ninth",
			"major ninth",
	};

	public static final String[] INTERVAL_NAMES_ABBREVIATIONS = {
			"o0",
			"m2",
			"M2",
			"m3",
			"M3",
			"t4",
			"tr",
			"t5",
			"m6",
			"M6",
			"m7",
			"M7",
			"o1",
			"m9",
			"M9",
	};

	public static final int[] INTERVAL_DISTANCE = {
			0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14
	};

	public static final int NOTE_64 = 1;

	public static final int NOTE_32 = NOTE_64 * 2;

	public static final int NOTE_32_DOT = NOTE_32 + (NOTE_32 / 2);

	public static final int NOTE_16 = NOTE_32 * 2;

	public static final int NOTE_16_DOT = NOTE_16 + (NOTE_16 / 2);

	public static final int NOTE_8 = NOTE_16 * 2;

	public static final int NOTE_8_DOT = NOTE_8 + (NOTE_8 / 2);

	public static final int NOTE_4 = NOTE_8 * 2;

	public static final int NOTE_4_DOT = NOTE_4 + (NOTE_4 / 2);

	public static final int NOTE_2 = NOTE_4 * 2;

	public static final int NOTE_2_DOT = NOTE_2 + (NOTE_2 / 2);

	public static final int NOTE_O = NOTE_2 * 2;

	public static final int NOTE_O_DOT = NOTE_O + (NOTE_O / 2);

	public static final int NOTE_OO = NOTE_O * 2;

	public static final int NOTE_OO_DOT = NOTE_OO + (NOTE_OO / 2);

	public static final int NOTE_OOOO = NOTE_OO * 2;

	public static final int NOTE_OOOO_DOT = NOTE_OOOO + (NOTE_OOOO / 2);

	public static final Map<Integer, String> noteDurationNameMap = new HashMap<Integer, String>();


	static {
		noteDurationNameMap.put(NOTE_64, "64th");
		noteDurationNameMap.put(NOTE_32, "32nd");
		noteDurationNameMap.put(NOTE_32_DOT, "dotted 32nd");
		noteDurationNameMap.put(NOTE_16, "16th");
		noteDurationNameMap.put(NOTE_16_DOT, "dotted 16th");
		noteDurationNameMap.put(NOTE_8, "8th");
		noteDurationNameMap.put(NOTE_8_DOT, "dotted 8th");
		noteDurationNameMap.put(NOTE_4, "quarter");
		noteDurationNameMap.put(NOTE_4_DOT, "dotted quarter");
		noteDurationNameMap.put(NOTE_2, "half");
		noteDurationNameMap.put(NOTE_2_DOT, "dotted half");
		noteDurationNameMap.put(NOTE_O, "whole");
		noteDurationNameMap.put(NOTE_O_DOT, "dotted whole");
		noteDurationNameMap.put(NOTE_OO, "double whole");
		noteDurationNameMap.put(NOTE_OO_DOT, "dotted double whole");
		noteDurationNameMap.put(NOTE_OOOO, "quadruple whole");
		noteDurationNameMap.put(NOTE_OOOO_DOT, "dotted quadruple whole");
	}

	// ******************************************************************************************************************
	// STATIC METHODS


	public static void sleep(int ms) {
		try {
			Thread.sleep(ms);
		}
		catch (InterruptedException e) {
			// ignored
		}
	}


	public static void sleep(long ms) {
		try {
			Thread.sleep(ms);
		}
		catch (InterruptedException e) {
			// ignored
		}
	}


	public static void sleep(float s) {
		try {
			Thread.sleep((long) (s * 1000.0));
		}
		catch (InterruptedException e) {
			// ignored
		}
	}


	public static void sleep(double s) {
		try {
			Thread.sleep((long) (s * 1000.0));
		}
		catch (InterruptedException e) {
			// ignored
		}
	}


	public static int calcMIDINoteID(double frequency) {
		return (int) (69f + PMath.log2(frequency / 440f));
	}


	public static String getNoteDurationName(int durationID) {
		return noteDurationNameMap.get(durationID);
	}


	public static int[] getScalePattern(String patternName) {
		return scalePatternMap.get(patternName);
	}


	public static double convertSecondsPerBeatToMillisPerBeat(double spb) {
		return (spb * 1000d);
	}


	public static double convertBeatsPerMinuteToMillisPerBeat(double bpm) {
		return (60000d / bpm);
	}


	public static double convertBeatsPerMinuteToNanosPerBeat(double bpm) {
		return (60000000000d / bpm);
	}

	// ******************************************************************************************************************
	// INSTANCE FIELDS

	public final AudioContext audioContext = new AudioContext();

	double[] toneArray = new double[tonesPerOctave * (octaveMax - octaveMin + 1)];

	double[][] octaveArray = new double[(octaveMax - octaveMin + 1)][tonesPerOctave];

	Map<Integer, List<Double>> octaveFrequencyMap = new HashMap<Integer, List<Double>>();

	Map<Double, Integer> frequencyOctaveMap = new HashMap<Double, Integer>();

	List<String> baseOctaveFrequencyNames = new ArrayList<String>();

	Map<String, Double> baseOctaveFrequencyMap = new HashMap<String, Double>();

	Map<String, Double> toneFrequencyMap = new HashMap<String, Double>();

	Map<Double, String> toneFrequencyNameMap = new HashMap<Double, String>();

	Map<Double, Integer> toneFrequencyIndexMap = new HashMap<Double, Integer>();

	Map<Integer, Double> toneIndexFrequencyMap = new HashMap<Integer, Double>();

	Map<String, Integer> intervalMap = new HashMap<String, Integer>();

	Map<Integer, String> intervalNameMap = new HashMap<Integer, String>();

	Map<Integer, String> intervalAbbrevMap = new HashMap<Integer, String>();

	Map<String, Scale> scaleMap = new HashMap<String, Scale>();

	List<String> scaleNameList = new ArrayList<String>();

	List<Scale> scaleList = new ArrayList<Scale>();

	int beatsPerMeasure = 4;

	int beatNoteValue = NOTE_4;

	double bpm = 120;

	double msPerBeat = 60000f / bpm;	// a 'beat' is the duration of a 'beatNoteValue' note

	double msPerTick = msPerBeat / (double) beatNoteValue;	// a 'tick' is the duration of a 64th note

	double nsPerBeat = 60000000000f / bpm;

	double nsPerTick = nsPerBeat / (double) beatNoteValue;	// a 'tick' is the duration of a 64th note


	public Musica() {
		setBPM(60);

		for (int interval = 0; interval < INTERVAL_DISTANCE.length; interval++) {
			String name = INTERVAL_NAMES[interval];
			String abbrev = INTERVAL_NAMES_ABBREVIATIONS[interval];
			int distance = INTERVAL_DISTANCE[interval];

			intervalMap.put(name.toLowerCase(), distance);
			intervalMap.put(abbrev, distance);
			intervalNameMap.put(distance, name.toLowerCase());
			intervalAbbrevMap.put(distance, abbrev);
		}

		for (int octave = octaveMin; octave <= octaveMax; octave++) {
			int oct = octave - octaveMin;
			for (int tone = 0; tone < tonesPerOctave; tone++) {
				int toneIdx = (oct * tonesPerOctave) + tone;
				double frequency = (double) (baseFrequency * Math.pow(2, (double) toneIdx / (double) tonesPerOctave));

				octaveArray[oct][tone] = frequency;
				List<Double> of = octaveFrequencyMap.get(oct);
				if (of == null) {
					of = new ArrayList<Double>();
					octaveFrequencyMap.put(oct, of);
				}
				of.add(frequency);
				frequencyOctaveMap.put(frequency, oct);

				toneArray[toneIdx] = frequency;
				toneFrequencyIndexMap.put(frequency, toneIdx);
				toneIndexFrequencyMap.put(toneIdx, frequency);

				String[] names = NOTE_NAMES_12TET[tone];
				for (int i = 0; i < names.length; i++) {
					if ((oct == octaveMin) && (i == 0)) {
						String baseName = names[0];
						toneFrequencyMap.put(baseName.toLowerCase(), frequency);
						toneFrequencyNameMap.put(frequency, baseName);
						baseOctaveFrequencyNames.add(baseName);
						baseOctaveFrequencyMap.put(baseName.toLowerCase(), frequency);
					}

					String name = names[i] + octave;
					toneFrequencyMap.put(name.toLowerCase(), frequency);
					toneFrequencyNameMap.put(frequency, name);
				}
			}
		}
	}


	public void start() {
		audioContext.start();
	}


	public void stop() {
		audioContext.stop();
	}


	public void setTimeSignature(int beatsPerMeasure, int beatNoteValue) {
		this.beatsPerMeasure = beatsPerMeasure;
		this.beatNoteValue = beatNoteValue;
		msPerTick = msPerBeat / (double) this.beatNoteValue;	// a 'tick' is the duration of a 64th note
		nsPerTick = nsPerBeat / (double) this.beatNoteValue;	// a 'tick' is the duration of a 64th note
	}


	public int getBeatNoteValue() {
		return beatNoteValue;
	}


	public int getBeatsPerMeasure() {
		return beatsPerMeasure;
	}


	public void setBPM(double bpm) {
		this.bpm = bpm;
		msPerBeat = convertBeatsPerMinuteToMillisPerBeat(this.bpm);	// a 'beat' is the duration of a 'beatNoteValue' note
		msPerTick = this.msPerBeat / (double) this.beatNoteValue;	// a 'tick' is the duration of a 64th note
		nsPerBeat = convertBeatsPerMinuteToNanosPerBeat(this.bpm);	// a 'beat' is the duration of a 'beatNoteValue' note
		msPerTick = this.nsPerBeat / (double) this.beatNoteValue;	// a 'tick' is the duration of a 64th note
	}


	public double getNanosPerBeat() {
		return convertBeatsPerMinuteToNanosPerBeat(this.bpm);
	}


	public double getMsPerBeat() {
		return convertBeatsPerMinuteToMillisPerBeat(this.bpm);
	}


	public double getMsPerTick() {
		return msPerTick;
	}


	public double getBPM() {
		return bpm;
	}


	public RTInput newRTInput() {
		return new RTInput(audioContext);
	}


	public Clock newClock(double msPerBeat) {
		return new Clock(audioContext, (float) msPerBeat);
	}


	public Clock newClockBPM(double bpm) {
		return newClock(convertBeatsPerMinuteToMillisPerBeat(bpm));
	}


	public Clock newClockSPB(double spb) {
		return newClock(convertSecondsPerBeatToMillisPerBeat(spb));
	}


	public void startClock(Clock clock) {
		getOut().addDependent(clock);
	}


	public void stopClock(Clock clock) {
		getOut().removeDependent(clock);
	}


	public double getNsPerTick() {
		return nsPerTick;
	}


	public double getSecondsPerTick() {
		return nsPerTick / 1e9;
	}


	public double getNoteDuration(int durationID) {
		return nsPerTick * durationID;
	}


	public Gain getOut() {
		return audioContext.out;
	}


	public List<String> getScaleNames() {
		return scaleNameList;
	}


	public List<Scale> getScales() {
		return scaleList;
	}


	public int getToneCount() {
		return toneArray.length;
	}


	public int getOctaveMin() {
		return octaveMin;
	}


	public int getOctaveMax() {
		return octaveMax;
	}


	public int getTonesPerOctave() {
		return tonesPerOctave;
	}


	public double getToneFrequency(int tone) {
		return toneIndexFrequencyMap.get(tone);
	}


	public double getToneFrequency(int octave, int tone) {
		return octaveArray[octave - octaveMin][tone];
	}


	public double getToneFrequency(String name) {
		return toneFrequencyMap.get(name.toLowerCase());
	}


	public int getToneFrequencyIndex(double frequency) {
		return toneFrequencyIndexMap.get(frequency);
	}


	public String getFrequencyToneName(double frequency) {
		return toneFrequencyNameMap.get(frequency);
	}


	public int getFrequencyOctave(double frequency) {
		return frequencyOctaveMap.get(frequency);
	}


	public List<Double> getOctaveFrequencies(int octave) {
		return octaveFrequencyMap.get(octave);
	}


	public String[][] getToneNames() {
		return NOTE_NAMES_12TET;
	}


	public String[] getSimpleToneNames() {
		String[] result = new String[NOTE_NAMES_12TET.length];
		for (int i = 0; i < result.length; i++) {
			result[i] = NOTE_NAMES_12TET[i][0];
		}
		return result;
	}


	public String getFrequencyName(double frequency) {
		return toneFrequencyNameMap.get(frequency);
	}


	public double getBeatDuration() {
		return (double) bpm / 60f;
	}


	public double getBeatDuration(int beatsPerMinute) {
		return (double) beatsPerMinute / 60f;
	}


	public Scale getScale(String name) {
		return scaleMap.get(name);
	}


	public Glide newGlide(double frequency) {
		return new Glide(audioContext, (float) frequency);
	}


	public Gain newGain(int inouts) {
		return new Gain(audioContext, inouts);
	}


	public Gain newGain(int inouts, Envelope env) {
		return new Gain(audioContext, inouts, env);
	}


	public Gain newGain(int inouts, double gain) {
		return new Gain(audioContext, inouts, (float) gain);
	}


	public Noise newNoise() {
		return new Noise(audioContext);
	}


	public Envelope newEnvelope() {
		return new Envelope(audioContext);
	}


	public Envelope newEnvelope(double initialValue) {
		return new Envelope(audioContext, (float) initialValue);
	}


	public void play(final EventList eventList) {
		final PrecisionClock pclock = new PrecisionClock();
		pclock.addListener(new PrecisionClockListener() {
			long startTime = -1L;

			long previousTime = -1L;


			public void tick(long tickCount, long currentTimeNanos, long cycleTimeNanos) {
				if (previousTime == -1L) {
					startTime = currentTimeNanos;
					previousTime = 0;
				}
				long now = currentTimeNanos - startTime;
				long prev = previousTime;
				previousTime = now + 1;

				List<Event> subs = eventList.getEvents(tickCount);
				if ((subs != null) && !subs.isEmpty()) {
					for (Event event : subs) {
						event.action();
					}
				}
			}
		});
		pclock.start(getSecondsPerTick());

//		Clock clock = new Clock(audioContext, getMsPerBeat());
//		clock.addMessageListener(
//				new Bead() {
//					long startTime = System.nanoTime();
//
//					long previousTime = 0;
//
//
//					public void messageReceived(Bead message) {
//
//						long now = (System.nanoTime() - startTime) / 1000000;
//						long prev = previousTime;
//						previousTime = now + 1;
//
//						List<Event> events = new ArrayList<Event>();
//						for (long i = prev; i <= now; i++) {
//							List<Event> subs = eventList.getEvents(i);
//							if (subs != null) {
//								events.addAll(subs);
//							}
//						}
//
//						if(!events.isEmpty()) {
//							Logger.log(Long.toString(now - prev));
//							for (Event event : events) {
//								Logger.log(event.getName());
//								event.action();
//							}
//						}
//					}
//				});
//		getOut().addDependent(clock);
	}

}